/**
 * 
 */
package truerefactor.graph;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * MethodNode - An implementation of CodeNode used to model a method in a code
 * graph.
 * 
 * @author Isaac Griffith
 */
public class MethodNode extends CodeNode {

    /**
     * Graph of all the statements in the method body
     */
    private CodeGraph statementGraph;
    /**
     * accessibility of the method
     */
    private Accessibility accessibility;
    /**
     * List of all the statements in the statement graph
     */
    private List<CodeNode> statements;
    /**
     * parent class of this method
     */
    private ClassNode parent;
    /**
     * Attributes this MethodNode is associated with inside its own class
     */
    private List<AttributeNode> assocAttrs;
    /**
     * Attributes this MethodNode is associated with outside its own class
     */
    private List<AttributeNode> otherAttrs;
    /**
     * Methods this MethodNode is associated with outside its own class
     */
    private List<MethodNode> otherMethods;
    /**
     * Methods this MethodNode is associated within its own class
     */
    private List<MethodNode> assocMethods;
    /**
     * Boolean flag specifying whether or not this method was declared static
     */
    private boolean isStatic;
    /**
     * String representing the return type of this method
     */
    private String retType;
    /**
     * Boolean flag specifying whether or not this method was declared abstract
     */
    private boolean isAbstract;
    /**
     * Boolean flag specifying that the return value of this method is primitive
     */
    private boolean isReturnPrimitive;
    /**
     * parameter information map
     */
    private Map<String, TypePair> paramTypeMap;
    /**
     * the simple identifier of this method
     */
    private String baseID;

    /**
     * Creates a new MethodNode instance with the given identifer and code, with
     * a default public accessibility
     * 
     * @param identifier
     *            method name
     * @param code
     *            code for the method
     */
    public MethodNode(String identifier, String code)
    {
        this(identifier, code, Accessibility.Public);
    }

    /**
     * @return the all attributes in the same class that this method uses
     */
    public List<AttributeNode> getAssocAttrs()
    {
        return assocAttrs;
    }

    /**
     * Creates a new MethodNode instance with the given identifier, code, and
     * accessibility
     * 
     * @param identifier
     *            name of the method
     * @param code
     *            code underlying the method
     * @param access
     *            accessiblity of the method
     */
    public MethodNode(String identifier, String code, Accessibility access)
    {
        this(CodeGraph.nextUID(), identifier, code, access);
    }

    /**
     * Creates a new MethodNode instance with the given unique id, base method
     * name, code, and accessibility
     * 
     * @param uid
     *            unique identifier
     * @param baseID
     *            simple method name
     * @param code
     *            underlying code
     * @param access
     *            accessibility
     */
    public MethodNode(long uid, String baseID, String code, Accessibility access)
    {
        super(uid, baseID, code);

        if (baseID == null)
        {
            this.baseID = "";
            identifier = "";
        }
        else
        {
            this.baseID = baseID;
        }
        statementGraph = new CodeGraph();
        setStatements(new ArrayList<CodeNode>());
        paramTypeMap = new HashMap<String, TypePair>();
        parent = null;
        accessibility = access;
        assocAttrs = new ArrayList<AttributeNode>();
        isAbstract = false;
        assocMethods = new ArrayList<MethodNode>();
        isReturnPrimitive = false;
        isStatic = false;
        otherAttrs = new ArrayList<AttributeNode>();
        otherMethods = new ArrayList<MethodNode>();
        retType = "";
    }

    /**
     * @return the parameters names of this method
     */
    public List<String> getParameters()
    {
        List<String> parameters = new ArrayList<String>();
        parameters.addAll(paramTypeMap.keySet());

        return parameters;
    }

    /**
     * Adds a new parameter to this method, with the given name, type, and
     * whether or not it is a primitive variable
     * 
     * @param name
     *            parameter name
     * @param type
     *            parameter type
     * @param primitive
     *            primitiveness flag
     */
    public void addParameter(String name, String type, boolean primitive)
    {
        paramTypeMap.put(name, new TypePair(type, primitive));
    }

    /**
     * Adds a new parameter to this method with the given name and type info
     * 
     * @param name
     *            parameter name
     * @param paramInfo
     *            type info containing the type and whether the parameter is
     *            primitive or not
     */
    public void addParameter(String name, TypePair paramInfo)
    {
        paramTypeMap.put(name, paramInfo);
    }

    /**
     * If a parameter with the given name exists for this method, then its type
     * information will be returned
     * 
     * @param name
     *            name of parameter to find
     * @return type info for the parameter with the given name, else a null is
     *         returned
     */
    public TypePair getParamInfo(String name)
    {
        return paramTypeMap.get(name);
    }

    /**
     * @return the parent class of this method
     */
    public ClassNode getParentClass()
    {
        return parent;
    }

    /**
     * Sets the parent class to the provided ClassNode
     * 
     * @param parent
     *            the new parent of this method
     */
    public void setParentClass(ClassNode cNode)
    {
        this.parent = cNode;
        if (cNode != null)
        {
            this.identifier = cNode.getIdentifier() + "." + baseID;
        }
    }

    /**
     * @return the simple unqualified method name
     */
    public String getBaseID()
    {
        return baseID;
    }

    /*
     * (non-Javadoc)
     * @see truerefactor.graph.CodeNode#add(truerefactor.graph.CodeNode)
     */
    @Override
    public void add(CodeNode child)
    {
        if (child instanceof StatementNode)
        {
            ((StatementNode) child).setParent(this);
            statementGraph.insert(child);
            getStatements().add(child);
        }
    }

    /*
     * (non-Javadoc)
     * @see truerefactor.graph.CodeNode#remove(truerefactor.graph.CodeNode)
     */
    @Override
    public void remove(CodeNode child)
    {
        if (child instanceof StatementNode)
        {
            statementGraph.remove(child);
            getStatements().remove(child);
        }

    }

    /*
     * (non-Javadoc)
     * @see truerefactor.graph.CodeNode#getChild(java.lang.String)
     */
    @Override
    public CodeNode getChild(String identfier)
    {
        CodeNode retVal = null;
        for (CodeNode cNode : getStatements())
        {
            if (cNode.getIdentifier().equals(identfier))
            {
                retVal = cNode;
                break;
            }
        }

        return retVal;
    }

    /**
     * @return the graph containing the statements of this method
     */
    public CodeGraph getStatementGraph()
    {
        return statementGraph;
    }

    /*
     * (non-Javadoc)
     * @see truerefactor.graph.CodeNode#getChildren()
     */
    @Override
    public List<CodeNode> getChildren()
    {
        return getStatements();
    }

    /**
     * @return the accessiblity of this method
     */
    public Accessibility getAccessibility()
    {
        return accessibility;
    }

    /**
     * Sets the accessibility of this method to the one provided
     * 
     * @param visibility
     *            new accessibility of this method
     */
    public void setAccessiblity(Accessibility visibility)
    {
        this.accessibility = visibility;
    }

    /**
     * Associates this mehtod with the provided attribute, if they are from the
     * same class. Then it adds this attribute to its list of associated
     * attributes
     * 
     * @param fNode
     *            new associate attribute
     */
    public void addAssociatedAttribute(AttributeNode fNode)
    {
        if (!assocAttrs.contains(fNode) && fNode.getParent().equals(this.parent))
        {
            // System.out.println("Adding Associated Attrib");
            assocAttrs.add(fNode);
            fNode.addAssociatedClassMethod(this);
        }
    }

    /**
     * Removes an associated attribute, only if that attribute is from the same
     * class
     * 
     * @param fNode
     *            attribute to be removed
     */
    public void removeAssociatedAttribute(AttributeNode fNode)
    {
        if (assocAttrs.contains(fNode))
        {
            assocAttrs.remove(fNode);
            fNode.removeAssociatedClassMethod(this);
        }
    }

    /**
     * Replaces the current statement graph with the provided one
     * 
     * @param statements
     *            new statement graph
     */
    public void setStatementGraph(CodeGraph statements)
    {
        if (statements == null)
            this.statementGraph = new CodeGraph();
        else
            this.statementGraph = statements;

        this.setStatements(statements.getNodes());
    }

    /**
     * Sets the flag declaring this method static or not.
     * 
     * @param isStatic
     *            new value for the isStatic flag
     */
    public void setStatic(boolean isStatic)
    {
        this.isStatic = isStatic;
    }

    /**
     * @return true if this method is declared static, false otherwise
     */
    public boolean isStatic()
    {
        return this.isStatic;
    }

    /**
     * @return return type of this method
     */
    public String getReturnType()
    {
        return retType;
    }

    /**
     * Sets the return type of this method to the specified type
     * 
     * @param retType
     *            new return type for this method
     */
    public void setReturnType(String retType)
    {
        this.retType = retType;
    }

    /**
     * Adds provided method node to the list of associated methods if the other
     * method is from another class
     * 
     * @param mNode
     *            method to add
     */
    public void addOtherMethod(MethodNode mNode)
    {
        if (!otherMethods.contains(mNode) && !mNode.getParentClass().equals(this.parent))
        {
            otherMethods.add(mNode);
        }
    }

    /**
     * Removes method node from the list of associated methods if the other
     * method is from another class
     * 
     * @param mNode
     *            method to be removed
     */
    public void removeOtherMethod(MethodNode mNode)
    {
        if (otherMethods.contains(mNode))
            otherMethods.remove(mNode);
    }

    /**
     * @return list of methods from other classes associated with this method
     */
    public List<MethodNode> getOtherMethods()
    {
        return otherMethods;
    }

    /**
     * Adds the specified method as an associate only if it is from the same
     * class
     * 
     * @param mNode
     *            method to be added
     */
    public void addAssociatedMethod(MethodNode mNode)
    {
        if (!assocMethods.contains(mNode) && mNode.getParentClass().equals(this.parent))
        {
            assocMethods.add(mNode);
        }
    }

    /**
     * removes the given method node from the list of associated methods for
     * this method if the provided method is from the same class
     * 
     * @param mNode
     *            method to be removed
     */
    public void removeAssociatedMethod(MethodNode mNode)
    {
        if (assocMethods.contains(mNode))
        {
            assocMethods.remove(mNode);
        }
    }

    /**
     * @return list of methods associated with this method from the same class
     */
    public List<MethodNode> getAssociatedMethods()
    {
        return assocMethods;
    }

    /**
     * Adds an associated attribute to this method, if that attribute is from
     * another class
     * 
     * @param aNode
     *            attribute to be added
     */
    public void addOtherAttribute(AttributeNode aNode)
    {
        if (!otherAttrs.contains(aNode) && !aNode.getParent().equals(this.parent))
        {
            otherAttrs.add(aNode);
            aNode.addAssociatedOtherMethod(this);
        }
    }

    /**
     * removes an association from an attribute, if that attribute is in another
     * class and is asscoiated with this method.
     * 
     * @param aNode
     *            AttributeNode to be removed
     */
    public void removeOtherAttribute(AttributeNode aNode)
    {
        if (otherAttrs.contains(aNode))
        {
            otherAttrs.remove(aNode);
            aNode.removeAssociatedOtherMethod(this);
        }
    }

    /**
     * @return the list of associated attributes from other classes
     */
    public List<AttributeNode> getOtherAttributes()
    {
        return otherAttrs;
    }

    /**
     * @return true if this method is abstract, false otherwise
     */
    public boolean isAbstract()
    {
        return isAbstract;
    }

    /**
     * Sets the method is abstract flag to the provided value
     * 
     * @param isAbstract
     *            new value of the abstract flag
     */
    public void setAbstract(boolean isAbstract)
    {
        this.isAbstract = isAbstract;
    }

    /**
     * @return true if the return type is a primitive type, false otherwise
     */
    public boolean isReturnPrimitive()
    {
        return isReturnPrimitive;
    }

    /**
     * sets the flag saying that the return type of this method is a primitive
     * type.
     * 
     * @param isReturnPrimitive
     *            new flag value for the primitive return type flag
     */
    public void setReturnPrimitive(boolean isReturnPrimitive)
    {
        this.isReturnPrimitive = isReturnPrimitive;
    }

    /**
     * @return list of all parameter info for this method
     */
    public List<TypePair> getAllParamInfo()
    {
        List<TypePair> retVal = new ArrayList<TypePair>();
        for (String param : paramTypeMap.keySet())
        {
            retVal.add(paramTypeMap.get(param));
        }

        return retVal;
    }

    /*
     * (non-Javadoc)
     * @see truerefactor.graph.CodeNode#clone()
     */
    @Override
    protected Object clone() throws CloneNotSupportedException
    {
        MethodNode node = new MethodNode(uid, new String(identifier), new String(retType), accessibility);
        node.setAbstract(isAbstract);
        node.setStatic(isStatic);
        node.setReturnPrimitive(isReturnPrimitive);

        List<String> params = getParameters();
        List<TypePair> paramInfo = getAllParamInfo();
        for (int i = 0; i < params.size(); i++)
        {
            node.addParameter(params.get(i), paramInfo.get(i).getType(), paramInfo.get(i).isPrimitive());
        }

        return node;
    }

    /**
     * Sets the list of statemetns to be the provided list
     * 
     * @param statements
     *            new set of statements for this method
     */
    public void setStatements(List<CodeNode> statements)
    {
        this.statements = statements;
    }

    /**
     * @return the list of all statements in this methods body
     */
    public List<CodeNode> getStatements()
    {
        return statements;
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    public String toString()
    {
        return baseID;
    }

    /**
     * @return map of temporary variable names to the statement nodes they are
     *         defined in.
     */
    public HashMap<String, StatementNode> findTempVars()
    {
        List<StatementNode> stmts = new ArrayList<StatementNode>();
        HashMap<String, StatementNode> retVal = new HashMap<String, StatementNode>();
        for (CodeNode node : getStatements())
        {
            if (node instanceof StatementNode)
            {
                stmts.add((StatementNode) node);
            }
        }

        for (StatementNode node : stmts)
        {
            String code = node.getCode();
            if (code.contains("="))
            {
                Pattern p = Pattern.compile("\\p{Alnum}+\\s+\\p{Alnum}+\\s*=");
                Matcher m = p.matcher(code);
                if (m.matches())
                {
                    code = code.substring(0, code.indexOf("="));
                    String var = code.substring(code.indexOf(" ")).trim();
                    retVal.put(var, node);
                }
            }
            else
            {
                if (code.contains(";"))
                    code = code.substring(0, code.indexOf(";"));

                Pattern p = Pattern.compile("\\p{Alnum}+\\s+\\p{Alnum}");
                Matcher m = p.matcher(code);
                if (m.matches())
                {
                    String var = code.substring(code.indexOf(" ")).trim();
                    retVal.put(var, node);
                }
            }
        }

        return retVal;
    }

    /**
     * Extracts parameter info from a set of statement nodes
     * 
     * @param params
     *            statement nodes to extract from
     * @return parameter info
     */
    public static HashMap<String, TypePair> extractParamInfo(Set<StatementNode> params)
    {
        HashMap<String, TypePair> paramInfo = new HashMap<String, TypePair>();

        for (StatementNode node : params)
        {
            String code = node.getCode();
            if (code.contains("="))
            {
                code = code.substring(0, code.indexOf("="));
            }

            String type = code.substring(0, code.indexOf(" "));
            code = code.substring(code.indexOf(" ") + 1);
            boolean primitive = false;
            if (Character.isLowerCase(type.charAt(0)))
            {
                primitive = true;
            }
            else
            {
                primitive = false;
            }

            if (code.contains(","))
            {
                while (code.indexOf(",") > 0)
                {
                    String var = code.substring(0, code.indexOf(","));
                    code = code.substring(code.indexOf(",") + 1);

                    paramInfo.put(var, new TypePair(type, primitive));
                }

                if (code.indexOf(";") > 0)
                {
                    String var = code.substring(0, code.indexOf(";"));
                    paramInfo.put(var, new TypePair(type, primitive));
                }
                else
                {
                    String var = code;
                    paramInfo.put(var, new TypePair(type, primitive));
                }
            }
            else
            {
                if (code.indexOf(";") > 0)
                {
                    String var = code.substring(0, code.indexOf(";"));
                    paramInfo.put(var, new TypePair(type, primitive));
                }
                else
                {
                    String var = code;
                    paramInfo.put(var, new TypePair(type, primitive));
                }
            }
        }

        return paramInfo;
    }

    public void setOtherMethods(List<MethodNode> newOtherMethods)
    {
        this.otherMethods = newOtherMethods;        
    }
    
    public void setOtherAttrs(List<AttributeNode> newOtherAttrs) {
        this.otherAttrs = newOtherAttrs;
    }
    
    public void setAssocMethods(List<MethodNode> newAssocMethods) {
        this.assocMethods = newAssocMethods;
    }
    
    public void setAssocAttrs(List<AttributeNode> newAssocAttrs) {
        this.assocAttrs = newAssocAttrs;
    }
}